/*
 * GMatch.java
 * Created on 18 juin 2007
 *
 * Glacéo Internet Platform
 * http://sourceforge.net/projects/chm/
 *
 * Copyright (c) 2005-2008, Jens and Jan Janke (VirtualHockey Project)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hockeo.vhbase.data.dao.match;

import java.math.BigDecimal;
import java.util.Random;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import com.hockeo.vhbase.conf.GRulesConstants;
import com.hockeo.vhbase.data.dao.IGEntity;
import com.hockeo.vhbase.data.dao.club.GClub;
import com.hockeo.vhbase.data.dao.season.GMatchday;
import com.hockeo.vhbase.data.dao.standings.GGoalsAccount;
import com.hockeo.vhbase.data.helper.GMatchResult;
import com.hockeo.vhbase.data.helper.GMatchType;

/**
 * Represents a single hockey match.
 *
 * @version $Id$
 * @author jjanke
 */
@Entity
@Table( name = "s_match" )
public class GMatch implements IGEntity, Comparable<GMatch>
{
  @Id
  @Column( name = "id" )
  @GeneratedValue( strategy = GenerationType.AUTO )
  private int        d_nId;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "matchday_id" )
  private GMatchday  d_matchday;

  /**
   * If this match is not part of a KO round, this field is <code>null</code>.
   */
  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "ko_match_id" )
  private GKOMatch   d_koMatch;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "club_home_id" )
  private GClub      d_clubHome;

  @ManyToOne( fetch = FetchType.LAZY )
  @JoinColumn( name = "club_away_id" )
  private GClub      d_clubAway;

  @Column( name = "goals_set_home" )
  private boolean    d_fGoalsSetHome      = false;

  @Column( name = "goals_set_away" )
  private boolean    d_fGoalsSetAway      = false;

  @Column( name = "played" )
  private boolean    d_fPlayed            = false;

  /**
   * Number of goals actually placed for the home team. May be different from the actual
   * score, if there was a shootout or the result had to be adjusted because of not enough
   * goals on the goals account.
   */
  @Column( name = "goals_placed_home" )
  private int        d_nGoalsPlacedHome   = 0;

  /**
   * Number of goals actually placed for the away team. May be different from the actual
   * score, if there was a shootout or the result had to be adjusted because of not enough
   * goals on the goals account.
   */
  @Column( name = "goals_placed_away" )
  private int        d_nGoalsPlacedAway   = 0;

  /**
   * Number of goals scored by the home team (official match result).
   */
  @Column( name = "goals_home" )
  private int        d_nGoalsHome         = 0;

  /**
   * Number of goals scored by the away team (official match result).
   */
  @Column( name = "goals_away" )
  private int        d_nGoalsAway         = 0;

  @Column( name = "goals_shootout_home" )
  private int        d_nGoalsShootoutHome = 0;

  @Column( name = "goals_shootout_away" )
  private int        d_nGoalsShootoutAway = 0;

  @Column( name = "points_home" )
  private int        d_nPointsHome        = 0;

  @Column( name = "points_away" )
  private int        d_nPointsAway        = 0;

  @Column( name = "cost_home" )
  private BigDecimal d_decCostHome        = GRulesConstants.ZERO_GOALS;

  @Column( name = "cost_away" )
  private BigDecimal d_decCostAway        = GRulesConstants.ZERO_GOALS;

  /**
   * Normally all matches are required. However in KO rounds, some matches may not be
   * required because the round winner can be determined in fewer matches.
   */
  @Column( name = "not_required" )
  private boolean    d_fNotRequired       = false;

  /** Default parameterless constructor. */
  protected GMatch()
  {}

  /**
   * Creates a new match.
   *
   * @param matchday the matchday to which the match should be attached
   * @param clubHome the club that plays at home
   * @param clubAway the club that is received
   */
  public GMatch( GMatchday matchday, GClub clubHome, GClub clubAway )
  {
    d_matchday = matchday;
    d_clubHome = clubHome;
    d_clubAway = clubAway;
  }

  /**
   * Creates a new match attached to a KO match.
   *
   * @param komatch the KO match this match belongs to
   * @param matchday the matchday to which the match should be attached
   * @param clubHome the club that plays at home
   * @param clubAway the club that is received
   */
  public GMatch( GKOMatch komatch, GMatchday matchday, GClub clubHome, GClub clubAway )
  {
    this( matchday, clubHome, clubAway );

    d_koMatch = komatch;
  }

  public GMatchday getMatchday()
  {
    return d_matchday;
  }

  public GKOMatch getKOMatch()
  {
    return d_koMatch;
  }

  public GClub getClubHome()
  {
    return d_clubHome;
  }

  public GClub getClubAway()
  {
    return d_clubAway;
  }

  public boolean isGoalsSetHome()
  {
    return d_fGoalsSetHome;
  }

  public boolean isGoalsSetAway()
  {
    return d_fGoalsSetAway;
  }

  public boolean isPlayed()
  {
    return d_fPlayed;
  }

  public int getGoalsHome()
  {
    return d_nGoalsHome;
  }

  public int getGoalsAway()
  {
    return d_nGoalsAway;
  }

  public int getGoalsShootoutHome()
  {
    return d_nGoalsShootoutHome;
  }

  public int getGoalsShootoutAway()
  {
    return d_nGoalsShootoutAway;
  }

  public int getPointsHome()
  {
    return d_nPointsHome;
  }

  public int getPointsAway()
  {
    return d_nPointsAway;
  }

  public BigDecimal getCostHome()
  {
    return d_decCostHome;
  }

  public BigDecimal getCostAway()
  {
    return d_decCostAway;
  }

  /**
   * Executes the match. Checks how many goals have been scored by both clubs and verifies
   * that the score is acceptable by using the provided goal accounts. If more goals than
   * allowed have been set, the goals are adjusted. If the match is drawn, a shootout
   * session is simulated. The two goals accounts that have been passed as parameter are
   * finally updated so that they reflect the amount of available goals after the match.
   *
   * The provided goals accounts are modified so that they consider the cost of this
   * match. I.e. the cost of this match is subtracted from the remaining goals on the
   * account.
   *
   * @param gaHome the goals account for the home team
   * @param gaAway the goals account for the away team
   */
  public void play( final GGoalsAccount gaHome, final GGoalsAccount gaAway )
  {
    // Transform the goals that have been placed into the match result
    //
    d_nGoalsHome = d_nGoalsPlacedHome;
    d_nGoalsAway = d_nGoalsPlacedAway;

    // Check match result and goals consumption
    //
    checkMatchResult( gaHome, gaAway );
    gaHome.subtract( d_decCostHome );
    gaAway.subtract( d_decCostAway );

    // Play shootout
    //
    playShoout( gaHome, gaAway );

    // Distribute points
    //
    if ( d_nGoalsHome > d_nGoalsAway )
    {
      if ( d_nGoalsShootoutHome > 0 )
      {
        d_nPointsHome = GMatchResult.VICTORY_AFTER_SHOOTOUT.getNumPoints();
        d_nPointsAway = GMatchResult.LOSS_AFTER_SHOOTOUT.getNumPoints();
      }
      else
      {
        d_nPointsHome = GMatchResult.VICTORY.getNumPoints();
        d_nPointsAway = GMatchResult.LOSS.getNumPoints();
      }
    }
    else
    {
      if ( d_nGoalsShootoutAway > 0 )
      {
        d_nPointsHome = GMatchResult.LOSS_AFTER_SHOOTOUT.getNumPoints();
        d_nPointsAway = GMatchResult.VICTORY_AFTER_SHOOTOUT.getNumPoints();
      }
      else
      {
        d_nPointsHome = GMatchResult.LOSS.getNumPoints();
        d_nPointsAway = GMatchResult.VICTORY.getNumPoints();
      }
    }

    // Mark the match as being played
    //
    d_fPlayed = true;
  }

  /**
   * Checks if a shootout session is necessary. If both teams have scored the same number
   * of goals, a shootout session is played.
   *
   * If a shootout session is needed, it is only executed if both teams have enough goals
   * left on their goals account. Otherwise, the team having more goals available wins
   * automatically.
   *
   * The goals account of the shootout winning team is automatically adjusted (i.e. the
   * cost of the shootout winning goal is subtracted).
   *
   * @param gaHome the goals account for the home team
   * @param gaAway the goals account for the away team
   */
  private void playShoout( final GGoalsAccount gaHome, final GGoalsAccount gaAway )
  {
    if ( d_nGoalsHome != d_nGoalsAway )
      return;

    final boolean fHomeGoalsOk = gaHome.getGoals().compareTo( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL ) >= 0;
    final boolean fAwayGoalsOk = gaAway.getGoals().compareTo( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL ) >= 0;

    if ( !fHomeGoalsOk && !fAwayGoalsOk )
    {
      // Both teams have not enough goals left. In this case, the team having more goals
      // left wins. If both
      // teams have the same amount of goals left, a shootout is organized and the winner
      // loses his remaining goals
      // regardless of whether there are enough left or not.
      //
      final int nComparison = gaHome.getGoals().compareTo( gaAway.getGoals() );
      if ( nComparison > 0 )
      {
        d_nGoalsShootoutHome++;
        d_nGoalsHome++;
        d_decCostHome = d_decCostHome.add( gaHome.setToZero() );
        return;
      }
      else if ( nComparison < 0 )
      {
        d_nGoalsShootoutAway++;
        d_nGoalsAway++;
        d_decCostAway = d_decCostAway.add( gaAway.setToZero() );
        return;
      }
    }
    else if ( fHomeGoalsOk && !fAwayGoalsOk )
    {
      d_nGoalsShootoutHome++;
      d_nGoalsHome++;
      gaHome.subtract( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
      d_decCostHome = d_decCostHome.add( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
      return;
    }
    else if ( !fHomeGoalsOk && fAwayGoalsOk )
    {
      d_nGoalsShootoutAway++;
      d_nGoalsAway++;
      gaAway.subtract( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
      d_decCostAway = d_decCostAway.add( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
      return;
    }

    // If this point is reached, a real shootout session is played
    //

    // First round - five attempts per team
    //
    for ( int i = 0; i < 5; i++ )
    {
      // There is 50/50 chance to mark a goal - if the random generator returns 1, a goal
      // is considered to be scored
      //
      d_nGoalsShootoutHome += new Random().nextInt( 2 );
      d_nGoalsShootoutAway += new Random().nextInt( 2 );
    }

    // 2nd round, as long as there is an equality we continue
    //
    while ( d_nGoalsShootoutHome == d_nGoalsShootoutAway )
    {
      d_nGoalsShootoutHome += new Random().nextInt( 2 );
      d_nGoalsShootoutAway += new Random().nextInt( 2 );
    }

    // Ok, the session is finished, let's analyze the results
    //
    if ( d_nGoalsShootoutHome > d_nGoalsShootoutAway )
    {
      d_nGoalsHome++;

      if ( fHomeGoalsOk )
      {
        gaHome.subtract( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
        d_decCostHome = d_decCostHome.add( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
      }
      else
        d_decCostHome = d_decCostHome.add( gaHome.setToZero() );
    }
    else
    {
      d_nGoalsAway++;

      if ( fAwayGoalsOk )
      {
        gaAway.subtract( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
        d_decCostAway = d_decCostAway.add( GRulesConstants.COST_SHOOTOUT_WINNING_GOAL );
      }
      else
        d_decCostAway = d_decCostAway.add( gaAway.setToZero() );
    }
  }

  /**
   * Checks the match result and adjusts it so that the number of scored goals is possible
   * within the number of available goals in the teams goal account. The method adjusts
   * the match result as long as necessary so that no team consumes more goals than
   * available on its respective goals account.
   *
   * The method adjusts the instance variables {@link #d_nGoalsHome},
   * {@link #d_nGoalsAway} as well as {@link #d_decCostHome} and {@link #d_decCostAway}.
   *
   * This method must be called before a potential shootout session is executed!
   *
   * This method calls itself recursively!
   *
   * @param gaHome the goals account for the home team
   * @param gaAway the goals account for the away team
   */
  private void checkMatchResult( final GGoalsAccount gaHome, final GGoalsAccount gaAway )
  {
    assert d_nGoalsShootoutHome == 0 && d_nGoalsShootoutAway == 0 : "The match result checking produces wrong results if called after a shootout.";
    assert !d_fPlayed : "The match result cannot be checked after a match has been marked as having been played.";

    // Base cost
    //
    d_decCostHome = GRulesConstants.COST_HOME_GOAL.multiply( BigDecimal.valueOf( d_nGoalsHome ) );
    d_decCostAway = GRulesConstants.COST_AWAY_GOAL.multiply( BigDecimal.valueOf( d_nGoalsAway ) );

    // Goals bonus (if applicable)
    //
    int nGoalsDiff = d_nGoalsHome - d_nGoalsAway;
    if ( nGoalsDiff > 0 )
    {
      final BigDecimal decBonusHome = GRulesConstants.BONUS_HOME_GOAL.multiply( BigDecimal.valueOf( nGoalsDiff ) );
      d_decCostHome = d_decCostHome.subtract( decBonusHome );
    }
    else if ( nGoalsDiff < 0 )
    {
      final BigDecimal decBonusAway = GRulesConstants.BONUS_AWAY_GOAL.multiply( BigDecimal.valueOf( -1 * nGoalsDiff ) );
      d_decCostAway = d_decCostAway.subtract( decBonusAway );
    }

    // Actual check
    //
    final boolean fHomeGoalsOk = d_nGoalsHome == 0 || gaHome.getGoals().compareTo( d_decCostHome ) > 0;
    final boolean fAwayGoalsOk = d_nGoalsAway == 0 || gaAway.getGoals().compareTo( d_decCostAway ) > 0;

    if ( fHomeGoalsOk && fAwayGoalsOk )
    {
      return;
    }
    else if ( fHomeGoalsOk && !fAwayGoalsOk )
    {
      d_nGoalsAway--;
      checkMatchResult( gaHome, gaAway );
    }
    else if ( !fHomeGoalsOk && fAwayGoalsOk )
    {
      d_nGoalsHome--;
      checkMatchResult( gaHome, gaAway );
    }
    else
    {
      // That's the most complicated case. Both teams have placed too many goals. We
      // reduce the number of goals of the team having scored less goals first and
      // recheck.
      //
      if ( d_nGoalsHome < d_nGoalsAway )
        d_nGoalsHome--;
      else if ( d_nGoalsHome > d_nGoalsAway )
        d_nGoalsAway--;
      else
      {
        // Both teams have scored the same amount of goals, we reduce the number
        // of goals of the team having overdrawn its goals account the most and recheck.
        //
        final double dDiffHome = d_decCostHome.doubleValue() - gaHome.getGoals().doubleValue();
        final double dDiffAway = d_decCostAway.doubleValue() - gaAway.getGoals().doubleValue();

        if ( dDiffHome > dDiffAway )
          d_nGoalsHome--;
        else
          d_nGoalsAway--;
      }

      checkMatchResult( gaHome, gaAway );
    }
  }

  /**
   * Returns <code>true</code> if the match has been played and the result has been
   * obtained by playing a shootout session. If the match has been decided without
   * shootout or is not even finished, <code>false</code> is returned.
   */
  public boolean isDecidedByShootout()
  {
    return isPlayed() && ( d_nGoalsShootoutHome > 0 || d_nGoalsShootoutAway > 0 );
  }

  /**
   * Returns the result of the match from the perspective of the club specified as
   * argument.
   *
   * @param club the club from whose perspective to determine the match's result
   * @return a {@link GMatchResult} constant
   * @throws IllegalStateException if the match has not yet been played
   * @throws IllegalArgumentException if the club passed as argument does not take part in
   *           the match
   */
  public GMatchResult getResult( GClub club )
  {
    if ( !isPlayed() )
      throw new IllegalStateException( "Cannot determine result because match has not yet been played." );

    if ( club.equals( d_clubHome ) )
    {
      if ( !isDecidedByShootout() )
      {
        if ( d_nGoalsHome > d_nGoalsAway )
          return GMatchResult.VICTORY;
        else
          return GMatchResult.LOSS;
      }
      else
      {
        if ( d_nGoalsShootoutHome > d_nGoalsShootoutAway )
          return GMatchResult.VICTORY_AFTER_SHOOTOUT;
        else
          return GMatchResult.LOSS_AFTER_SHOOTOUT;
      }
    }

    if ( club.equals( d_clubAway ) )
    {
      if ( !isDecidedByShootout() )
      {
        if ( d_nGoalsHome < d_nGoalsAway )
          return GMatchResult.VICTORY;
        else
          return GMatchResult.LOSS;
      }
      else
      {
        if ( d_nGoalsShootoutHome < d_nGoalsShootoutAway )
          return GMatchResult.VICTORY_AFTER_SHOOTOUT;
        else
          return GMatchResult.LOSS_AFTER_SHOOTOUT;
      }
    }

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Returns the team that is the winner of this match.
   *
   * @return the club that won the match or <code>null</code> if this method is called
   *         although the match has not yet been played and analysed
   */
  public GClub getWinner()
  {
    if ( !isPlayed() )
      return null;

    assert d_nGoalsHome != d_nGoalsAway : "Home and away goals cannot be equal. There must always be a winner!";

    if ( d_nGoalsHome > d_nGoalsAway )
      return d_clubHome;
    else
      return d_clubAway;
  }

  /**
   * Returns the team that is the loser of this match.
   *
   * @return the club that lost the match or <code>null</code> if this method is called
   *         although the match has not yet been played and analysed
   */
  public GClub getLoser()
  {
    if ( !isPlayed() )
      return null;

    assert d_nGoalsHome != d_nGoalsAway : "Home and away goals cannot be equal. There must always be a winner!";

    if ( d_nGoalsHome < d_nGoalsAway )
      return d_clubHome;
    else
      return d_clubAway;
  }

  /**
   * Returns the club playing against the specified club in this match.
   *
   * @param club one of the two clubs taking part in the match
   * @return the other club taking part in the match
   * @throws IllegalArgumentException if the club passed as argument does not take part in
   *           the match
   */
  public GClub getOpponent( final GClub club )
  {
    if ( club.equals( d_clubHome ) )
      return d_clubAway;

    if ( club.equals( d_clubAway ) )
      return d_clubHome;

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Returns whether the club passed as argument plays at home or on the road in this
   * match.
   *
   * @param club one of the two clubs taking part in the match
   * @return one of the two constants {@link GMatchType#HOME} or {@link GMatchType#AWAY}
   * @throws IllegalArgumentException if the club passed as argument does not take part in
   *           the match
   */
  public GMatchType getMatchType( final GClub club )
  {
    if ( club.equals( d_clubHome ) )
      return GMatchType.HOME;

    if ( club.equals( d_clubAway ) )
      return GMatchType.AWAY;

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Returns the cost of one goal for the given club participating in this match.
   *
   * @param club the club for which to check how much does a goal cost
   * @return the cost of one goal
   * @throws IllegalArgumentException if the club passed as argument does not take part in
   *           the match
   */
  public BigDecimal getCostOfOneGoal( final GClub club )
  {

    if ( club.equals( d_clubHome ) )
      return GRulesConstants.COST_HOME_GOAL;

    if ( club.equals( d_clubAway ) )
      return GRulesConstants.COST_AWAY_GOAL;

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Returns the number of goals placed or scored for the given club.
   *
   * @param club the club for which to return the goals
   * @return the number of goals placed or scored by the specified club
   * @throws IllegalStateException if no goals have been placed so far
   * @throws IllegalArgumentException if the club passed as argument does not take part in
   *           the match
   */
  public int getNumGoals( final GClub club )
  {
    if ( club.equals( d_clubHome ) )
    {
      if ( d_fPlayed )
        return d_nGoalsHome;
      else if ( d_fGoalsSetHome )
        return d_nGoalsPlacedHome;

      throw new IllegalStateException( "No goals have been placed for club " + club.getId() + " and match " + getId() );
    }

    if ( club.equals( d_clubAway ) )
    {
      if ( d_fPlayed )
        return d_nGoalsAway;
      else if ( d_fGoalsSetAway )
        return d_nGoalsPlacedAway;

      throw new IllegalStateException( "No goals have been placed for club " + club.getId() + " and match " + getId() );
    }

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Returns the number of goals placed or scored for the opponent of the given club.
   *
   * @param club the club for which the opponent's goals should be returned
   * @return the number of goals placed or scored by the specified club's opponent
   * @throws IllegalStateException if no goals have been placed so far
   * @throws IllegalArgumentException if the club passed as argument does not take part in
   *           the match
   */
  public int getNumGoalsOpponent( final GClub club )
  {
    if ( club.equals( d_clubHome ) )
    {
      if ( d_fPlayed )
        return d_nGoalsAway;
      else if ( d_fGoalsSetAway )
        return d_nGoalsPlacedAway;

      throw new IllegalStateException( "No goals have been placed for club " + d_clubAway.getId() + " and match "
          + getId() );
    }

    if ( club.equals( d_clubAway ) )
    {
      if ( d_fPlayed )
        return d_nGoalsHome;
      else if ( d_fGoalsSetHome )
        return d_nGoalsPlacedHome;

      throw new IllegalStateException( "No goals have been placed for club " + d_clubHome.getId() + " and match "
          + getId() );
    }

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Returns the cost which has been produced for the given club by this match.
   *
   * @param club the club for which to return the cost
   * @return the cost of this match in goals
   * @throws IllegalStateException if the match has not yet been played
   * @throws IllegalArgumentException if the club passed as argument does not take part in
   *           the match
   */
  public BigDecimal getCost( final GClub club )
  {
    if ( club.equals( d_clubHome ) )
    {
      if ( d_fPlayed )
        return d_decCostHome;

      throw new IllegalStateException( "The match " + getId() + " has not yet been played." );
    }

    if ( club.equals( d_clubAway ) )
    {
      if ( d_fPlayed )
        return d_decCostAway;

      throw new IllegalStateException( "The match " + getId() + " has not yet been played." );
    }

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Places goals for the specified club.
   *
   * @param club the club for which to place goals
   * @param nGoals the amount of goals to be placed
   */
  public void placeGoals( final GClub club, final int nGoals )
  {
    if ( d_fPlayed )
      throw new IllegalStateException( "The match with ID " + d_nId
          + " has already been played. Placing goals is not possible anymore!" );

    if ( club.equals( d_clubHome ) )
    {
      d_nGoalsPlacedHome = nGoals;
      d_fGoalsSetHome = true;
      return;
    }

    if ( club.equals( d_clubAway ) )
    {
      d_nGoalsPlacedAway = nGoals;
      d_fGoalsSetAway = true;
      return;
    }

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );
  }

  /**
   * Places the goals for both teams. Goals are only placed if the amount of goals is not
   * -1. The latter means that no goals should be set.
   *
   * @param nGoalsHome the amount of home goals to be placed or -1 to leave unchanged
   * @param nGoalsAway the amount of away goals to be placed of -1 to leave unchanged
   */
  public void placeGoals( final int nGoalsHome, final int nGoalsAway )
  {
    if ( d_fPlayed )
      throw new IllegalStateException( "The match with ID " + d_nId
          + " has already been played. Placing goals is not possible anymore!" );

    if ( nGoalsHome != -1 )
    {
      d_nGoalsPlacedHome = nGoalsHome;
      d_fGoalsSetHome = true;
    }

    if ( nGoalsAway != -1 )
    {
      d_nGoalsPlacedAway = nGoalsAway;
      d_fGoalsSetAway = true;
    }
  }

  /**
   * Indicates if goals have already been set for the given club.
   *
   * @param club the club for which to check if goals have already been set
   * @return <code>true</code> if goals have already been set for the specified club,
   *         <code>false</code> otherwise
   */
  public boolean isGoalsSet( GClub club )
  {
    if ( club.equals( d_clubHome ) )
      return d_fGoalsSetHome;

    if ( club.equals( d_clubAway ) )
      return d_fGoalsSetAway;

    throw new IllegalArgumentException( "The club " + club.getId() + " does not participate in match no. " + d_nId );

  }

  public boolean isNotRequired()
  {
    return d_fNotRequired;
  }

  public void setNotRequired( boolean flag )
  {
    d_fNotRequired = flag;
  }

  public int getId()
  {
    return d_nId;
  }

  public Class<? extends IGEntity> getGEntityClass()
  {
    return GMatch.class;
  }

  public boolean instanceOf( Class<? extends IGEntity> classOther )
  {
    return classOther.isAssignableFrom( getGEntityClass() );
  }

  /*
   * @see java.lang.Object#equals(java.lang.Object)
   */
  @Override
  public boolean equals( Object obj )
  {
    try
    {
      return d_nId == ( (GMatch) obj ).getId();
    }
    catch ( Exception ex ) // especially ClassCastException(s) may happen
    {
      return false;
    }
  }

  /*
   * @see java.lang.Object#hashCode()
   */
  @Override
  public int hashCode()
  {
    return d_nId;
  }

  /**
   * Returns the match id followed by the names of the two clubs affronting each other.
   * The home team is mentionned first.
   *
   * @see java.lang.Object#toString()
   */
  @Override
  public String toString()
  {
    return String.valueOf( d_nId ) + " ( " + d_clubHome.getName() + " - " + d_clubAway.getName() + " )";
  }

  /**
   * Orders matches by matchday and then by match ID.
   */
  public int compareTo( GMatch other )
  {
    int nTmp = this.getMatchday().compareTo( other.getMatchday() );
    if ( nTmp != 0 )
      return nTmp;

    return this.getId() - other.getId();
  }
}
